/*******************************************************************************
 * Copyright (c) 2008 University of Illinois at Urbana-Champaign and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     UIUC - Initial API and implementation
 *******************************************************************************/
package org.eclipse.photran.internal.core.refactoring;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.photran.core.IFortranAST;
import org.eclipse.photran.internal.core.analysis.binding.Definition;
import org.eclipse.photran.internal.core.analysis.binding.ScopingNode;
import org.eclipse.photran.internal.core.lexer.Token;
import org.eclipse.photran.internal.core.parser.ASTCommonBlockNameNode;
import org.eclipse.photran.internal.core.parser.ASTCommonBlockNode;
import org.eclipse.photran.internal.core.parser.ASTCommonBlockObjectNode;
import org.eclipse.photran.internal.core.parser.ASTCommonStmtNode;
import org.eclipse.photran.internal.core.parser.ASTModuleNode;
import org.eclipse.photran.internal.core.parser.ASTTypeDeclarationStmtNode;
import org.eclipse.photran.internal.core.parser.ASTUseStmtNode;
import org.eclipse.photran.internal.core.parser.GenericASTVisitor;
import org.eclipse.photran.internal.core.parser.IASTListNode;
import org.eclipse.photran.internal.core.parser.IASTNode;
import org.eclipse.photran.internal.core.parser.ISpecificationPartConstruct;
import org.eclipse.photran.internal.core.parser.ISpecificationStmt;
import org.eclipse.photran.internal.core.refactoring.infrastructure.FortranEditorRefactoring;
import org.eclipse.photran.internal.core.reindenter.Reindenter;
import org.eclipse.photran.internal.core.reindenter.Reindenter.Strategy;
import org.eclipse.photran.internal.core.vpg.PhotranTokenRef;
import org.eclipse.photran.internal.core.vpg.PhotranVPG;

/**
 * Refactoring to move a COMMON block into a module.
 * <p>
 * THIS REFACTORING IS INCOMPLETE.  IT *DOES NOT* WORK CORRECTLY YET.
 *
 * @author Jeff Overbey
 * @author Ashley Kasza - externalized strings
 */
public class MoveCommonToModuleRefactoring extends FortranEditorRefactoring
{
    private static final String CRLF = System.getProperty("line.separator"); //$NON-NLS-1$

    private ASTCommonBlockNode commonBlockToMove = null;
    private String nameOfCommonBlockToMove = null;
    private String newModuleName = null;
    private Set<IFile> affectedFiles = null;

    @Override
    public String getName()
    {
        return Messages.MoveCommonToModuleRefactoring_Name;
    }

    ///////////////////////////////////////////////////////////////////////////
    // User-Accessible Parameters
    ///////////////////////////////////////////////////////////////////////////

    public String getSuggestedNewModuleName()
    {
        assert commonBlockToMove != null && nameOfCommonBlockToMove != null;

        return nameOfCommonBlockToMove.equals("") ? "common" : nameOfCommonBlockToMove; //$NON-NLS-1$ //$NON-NLS-2$
    }

    @UserInputString(label="Create a module named ", defaultValueMethod="getSuggestedNewModuleName")
    public void setNewModuleName(String name)
    {
        assert name != null;

        this.newModuleName = name;
    }

    ///////////////////////////////////////////////////////////////////////////
    // Initial Preconditions
    ///////////////////////////////////////////////////////////////////////////

    @Override
    protected void doCheckInitialConditions(RefactoringStatus status, IProgressMonitor pm) throws PreconditionFailure
    {
        ensureProjectHasRefactoringEnabled(status);

        findEnclosingCommonBlock();
        determineEnclosingCommonBlockName();
        determineAffectedFiles();

        // TODO: Require implicit none?

        // TODO: Make sure all uses of this common block are identical (names, types)
        // so that USE-ing the module will not cause naming conflicts
    }

    private void findEnclosingCommonBlock() throws PreconditionFailure
    {
        Token enclosingToken = findEnclosingToken(this.astOfFileInEditor, this.selectedRegionInEditor);
        if (enclosingToken == null)
            fail(Messages.MoveCommonToModuleRefactoring_SelectVarOrBlockInCommonStmt);

        commonBlockToMove = enclosingToken.findNearestAncestor(ASTCommonBlockNode.class);
        if (commonBlockToMove == null)
            fail(Messages.MoveCommonToModuleRefactoring_SelectVarOrBlockInCommonStmt);
    }

    private void determineEnclosingCommonBlockName()
    {
        nameOfCommonBlockToMove = getCommonBlockName(commonBlockToMove);
    }

    private String getCommonBlockName(ASTCommonBlockNode commonBlockToMove)
    {
        ASTCommonBlockNameNode commonBlockNameToken = commonBlockToMove.getName();
        if (commonBlockNameToken != null)
            return commonBlockNameToken.getCommonBlockName().getText();
        else
            return ""; //$NON-NLS-1$
    }

    private void determineAffectedFiles() throws PreconditionFailure
    {
        affectedFiles = new HashSet<IFile>();
        affectedFiles.add(fileInEditor);
        affectedFiles.addAll(vpg.findFilesThatUseCommonBlock(nameOfCommonBlockToMove));
    }

    ///////////////////////////////////////////////////////////////////////////
    // Final Preconditions
    ///////////////////////////////////////////////////////////////////////////

    @Override
    protected void doCheckFinalConditions(RefactoringStatus status, IProgressMonitor pm) throws PreconditionFailure
    {
        assert commonBlockToMove != null && nameOfCommonBlockToMove != null && affectedFiles != null;
        assert newModuleName != null;

        if (!isValidIdentifier(newModuleName)) fail(Messages.bind(Messages.MoveCommonToModuleRefactoring_InvalidIdentifier, newModuleName));
    }

    ///////////////////////////////////////////////////////////////////////////
    // Change
    ///////////////////////////////////////////////////////////////////////////

    @Override
    protected void doCreateChange(IProgressMonitor pm) throws CoreException, OperationCanceledException
    {
        assert commonBlockToMove != null && nameOfCommonBlockToMove != null && affectedFiles != null;
        assert newModuleName != null;

        try
        {
            for (IFile file : affectedFiles)
            {
                if (file.equals(fileInEditor))
                    createModule();

                replaceCommonBlockWithModuleUseIn(file);
                addChangeFromModifiedAST(file, pm);
            }
        }
        finally
        {
            vpg.releaseAllASTs();
        }
    }

    private void createModule()
    {
        ASTModuleNode module = createEmptyModule();

        for (ASTCommonBlockObjectNode obj : commonBlockToMove.getCommonBlockObjectList())
            populateModuleWithDeclarations(module, obj.getVariableName());

        addModuleAtBeginningOfFile(module);
    }

    private ASTModuleNode createEmptyModule()
    {
        String moduleSource =
            "module " + newModuleName + CRLF + //$NON-NLS-1$
            Reindenter.defaultIndentation() + "implicit none" + CRLF + //$NON-NLS-1$
            "end module " + newModuleName + CRLF; //$NON-NLS-1$
        return (ASTModuleNode)parseLiteralProgramUnit(moduleSource);
    }

    private void populateModuleWithDeclarations(ASTModuleNode module, Token variable)
    {
        List<ISpecificationPartConstruct> specificationStmts = findSpecificationStmtsFor(variable);
        module.getModuleBody().addAll(specificationStmts);
    }

    private List<ISpecificationPartConstruct> findSpecificationStmtsFor(Token variable)
    {
        List<Definition> defs = variable.resolveBinding();
        if (defs.size() == 1)
            return findSpecificationStmtsFor(defs.get(0));
        else
            throw new Error(); // TODO
    }

    private List<ISpecificationPartConstruct> findSpecificationStmtsFor(Definition def)
    {
        List<ISpecificationPartConstruct> result = new LinkedList<ISpecificationPartConstruct>();

        ASTTypeDeclarationStmtNode typeDecl = def.getTokenRef().findToken().findNearestAncestor(ASTTypeDeclarationStmtNode.class);
        if (typeDecl != null)
            result.add((ISpecificationPartConstruct)typeDecl.clone());

        for (PhotranTokenRef tokRef : def.findAllReferences(false))
        {
            ISpecificationStmt enclosingSpecStmt = tokRef.findToken().findNearestAncestor(ISpecificationStmt.class);
            if (enclosingSpecStmt != null && !(enclosingSpecStmt instanceof ASTCommonStmtNode))
                result.add((ISpecificationPartConstruct)enclosingSpecStmt.clone());
        }

        return result;
    }

    private void addModuleAtBeginningOfFile(ASTModuleNode module)
    {
        astOfFileInEditor.getRoot().getProgramUnitList().add(0, module);
        Reindenter.reindent(module, astOfFileInEditor, Strategy.REINDENT_EACH_LINE);
    }

    private void replaceCommonBlockWithModuleUseIn(IFile file)
    {
        IFortranAST ast = vpg.acquireTransientAST(file);

        for (ASTCommonBlockNode commonBlock : findCommonBlocksWithCorrectNameIn(ast))
        {
            removeSpecificationStmtsForCommonBlockVars(commonBlock);

            ASTCommonStmtNode enclosingCommonStmt = commonBlock.findNearestAncestor(ASTCommonStmtNode.class);

            addUseStmtAtBeginningOfScopeContaining(enclosingCommonStmt, ast);

            if (commonStmtContainsOnlyOneCommonBlock(enclosingCommonStmt))
                enclosingCommonStmt.removeFromTree();
            else
                commonBlock.removeFromTree();
        }
    }

    private List<ASTCommonBlockNode> findCommonBlocksWithCorrectNameIn(IFortranAST ast)
    {
        // Note that we must traverse the tree *before* we start changing it
        // to prevent ConcurrentModificationExceptions on list nodes

        final List<ASTCommonBlockNode> result = new LinkedList<ASTCommonBlockNode>();
        ast.accept(new GenericASTVisitor()
        {
            @Override
            public void visitASTCommonBlockNode(ASTCommonBlockNode commonBlock)
            {
                if (commonBlockHasSameName(commonBlock))
                    result.add(commonBlock);
            }
        });
        return result;
    }

    private void removeSpecificationStmtsForCommonBlockVars(ASTCommonBlockNode commonBlock)
    {
        // TODO: Should only remove this variable, not the entire spec statement
        //   common a, b
        //   integer :: a, b
        // will cause an IllegalStateException since the decl statement will be removed twice

        for (ASTCommonBlockObjectNode obj : commonBlock.getCommonBlockObjectList())
            for (ISpecificationPartConstruct specStmt : findSpecificationStmtsFor(obj.getVariableName()))
                try
        {
                specStmt.removeFromTree();
        }
        catch (IllegalStateException e)
        {
            // FIXME HACK -- Ignore
        }
    }

    @SuppressWarnings("unchecked")
    private void addUseStmtAtBeginningOfScopeContaining(ASTCommonStmtNode enclosingCommonStmt, IFortranAST ast)
    {
        ASTUseStmtNode useStmt = (ASTUseStmtNode)parseLiteralStatement("use " + newModuleName); //$NON-NLS-1$

        ScopingNode enclosingScope = enclosingCommonStmt.findNearestAncestor(ScopingNode.class);
        ((IASTListNode<IASTNode>)enclosingScope.getBody()).add(0, useStmt);

        Reindenter.reindent(useStmt, ast);
    }

    private boolean commonBlockHasSameName(ASTCommonBlockNode commonBlock)
    {
        String cnameOfThisCommonBlock = PhotranVPG.canonicalizeIdentifier(getCommonBlockName(commonBlock));
        String cnameOfCommonBlockToMove = PhotranVPG.canonicalizeIdentifier(nameOfCommonBlockToMove);
        return cnameOfThisCommonBlock.equals(cnameOfCommonBlockToMove);
    }

    private boolean commonStmtContainsOnlyOneCommonBlock(ASTCommonStmtNode enclosingCommonStmt)
    {
        return enclosingCommonStmt.getCommonBlockList().size() == 1;
    }
}
